package no.ffi.tpm.utils;

import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.PublicKey;
import java.util.Hashtable;
import java.util.UUID;
import no.ffi.gismoidm.attrex.BooleanOperand;
import no.ffi.gismoidm.attrex.Parser;
import no.ffi.tpm.Config;
import no.ffi.gismoidm.invocations.ServiceInvocation;
import no.ffi.gismoidm.invocations.ServiceInvocationFactory;
import no.ffi.gismoidm.utils.*;
import no.ffi.gismoidm.utils.GismoIdmException;


import no.ffi.tpm.crypto.CryptoOperations;
import no.ffi.tpm.crypto.CryptoOperationsFactory;


/**
 *
 * @author anders
 */
public class ClientProxy {

    private CryptoOperations crOp;
    private PublicKey trustAnchorPublicKey;
    private Hashtable identityStatements, clockCounters;
    
    public ClientProxy(String configFilename) throws Exception {
        if (configFilename != null && !configFilename.equals(""))
            System.setProperty("JIDM.CONFIG", configFilename);
        crOp = CryptoOperationsFactory.getInstance(CryptoOperationsFactory.JCE,"Federico");
        trustAnchorPublicKey = crOp.getTrustAnchor();
        identityStatements = new Hashtable();
        clockCounters = new Hashtable();
    }

    public static IdentityStatement getIdentityStatement(Serializable request,String idpURL,CryptoOperations crOp) throws GismoIdmException {
        Object f=null;
        try {
            f = goHTTP(idpURL,request);
        } catch (Exception e) { throw new GismoIdmException(e.getMessage());}
        
        // The response object may need to be decrypted
        if (f instanceof EncryptedIdentityStatement) {
            try {
                EncryptedIdentityStatement encIs = (EncryptedIdentityStatement)f;
                IdentityStatement fa = encIs.decrypt(crOp);
                return fa;
            } catch (Exception e) { e.printStackTrace(); }
        }    
        else if (f instanceof IdentityStatement) {
            IdentityStatement fa = (IdentityStatement)f;
            return fa;
            
        } else if (f instanceof GismoIdmException) { // or a subclass exception
            throw (GismoIdmException)f;
        }
        return null; // dummy
    }

    public IdentityStatement getLocalIdentityStatement() throws GismoIdmException{
        String idpURL = Config.getProperty("IDP_URL");
        Serializable request = null;
        try { // IS request may be a TPM-based key or a DN
            request = crOp.getISrequest();     
        } catch (Exception e) { throw new GismoIdmException(e.getMessage()); }
        
        IdentityStatement is = getIdentityStatement(request,idpURL,crOp);
        is.validateIdentityStatement(trustAnchorPublicKey);
        long clockCounter = is.getIssuingTimestamp() - System.currentTimeMillis();
        clockCounters.put(idpURL, clockCounter);
        identityStatements.put(idpURL, is);
        return is;
    }
    
    public IdentityStatement getCrossCOIIdentityStatement(String appPrefix, IdentityStatement localIS) throws GismoIdmException {
        String xidpURL = Config.getProperty(appPrefix + "_IDP_URL");
        if (xidpURL == null) return null;
        IdentityStatement is = getIdentityStatement(localIS,xidpURL,crOp);
        is.validateIdentityStatement(trustAnchorPublicKey);
        long clockCounter = is.getIssuingTimestamp() - System.currentTimeMillis();
        clockCounters.put(xidpURL, clockCounter);
        identityStatements.put(xidpURL, is);
        return is;
    }


//    IdentityStatement crossAss = null;
    public Serializable invokeApplicationService(String appPrefix, Serializable request, String accessRequirements) throws Exception {
        IdentityStatement localIdentityStatement, crossCOIIdentityStatement, tempIS;
        long clockCounter;
    
        BooleanOperand clientReqs = Parser.parse(accessRequirements);
        String localIdpURL = Config.getProperty("IDP_URL");
        localIdentityStatement = (IdentityStatement)identityStatements.get(localIdpURL);

        // Get info from config file on xIdP, serviceURL, serviceId and authentication protocol
        String xIdpURL = Config.getProperty(appPrefix + "_IDP_URL");
        // If no special idp url is defined, then this is a local COI service
        if (xIdpURL==null) { 
            crossCOIIdentityStatement = null;
        } else {
            crossCOIIdentityStatement = (IdentityStatement)identityStatements.get(xIdpURL);
        }
        String serviceURL = Config.getProperty(appPrefix + "_SERVICE_URL");
        String serviceID  = Config.getProperty(appPrefix + "_SERVICE_ID");
        String protocolClass = Config.getProperty(appPrefix + "_PROTOCOL_CLASS","STATEFUL");
        
        // Do we have a valid identity statement? Renew also if the xCOI-IS is about to expire
//        if (localIdentityStatement == null)
//            System.out.println("My IS has not been issued yet");
//        else System.out.println("My IS expires in : " + 
//                (localIdentityStatement.getNotAfter().getTime()
//                -System.currentTimeMillis())/1000 + " seconds");
//        if (crossCOIIdentityStatement == null) System.out.println("There is no xCOI IS issued");
//        else System.out.println("xCOI IS expires in : " + 
//                (crossCOIIdentityStatement.getNotAfter().getTime()
//                -System.currentTimeMillis())/1000 + " seconds");
        
        if (localIdentityStatement==null || 
               localIdentityStatement.identityStatementExpired(12) || // TODO Or a max skew variable?
               // NB Never set the parameter value of the following statement to a higher
               // number than the property value of IDENTITYSTATEMENT_XD_FUTURE_SLACK in the IdP
               (crossCOIIdentityStatement != null && crossCOIIdentityStatement.identityStatementExpired(0))) {
            localIdentityStatement = getLocalIdentityStatement();
        }    

        // Obtain a guest identity statement if necessary
        if ((xIdpURL!=null) && (crossCOIIdentityStatement == null || crossCOIIdentityStatement.identityStatementExpired(0))) {
            crossCOIIdentityStatement = getCrossCOIIdentityStatement(appPrefix,localIdentityStatement);
        }
        
        IdentityStatement validationIS = null;
        if (xIdpURL != null) {
            tempIS = crossCOIIdentityStatement.getGuestPart();
            validationIS = crossCOIIdentityStatement.getCrossCOIPart();
            clockCounter = (long)clockCounters.get(xIdpURL);
        } else {
            tempIS = localIdentityStatement;
            clockCounter = (long)clockCounters.get(localIdpURL);
        }

        ServiceInvocationFactory sif = new ServiceInvocationFactory(protocolClass);
        ServiceInvocation sio = sif.createRequestInstance
                (request, tempIS, crOp, clockCounter);
        Object f=null;
        UUID requestUuid = sio.getNonce();
        
        f = goHTTP(serviceURL,sio);

        if (f instanceof ServiceInvocation) {
            sio = (ServiceInvocation)f;
            sio.validateResponse(requestUuid, validationIS, serviceID,clientReqs,crOp);
            
        } else if (f instanceof Exception) throw (Exception)f;
        return sio.getReturns();
    }

    private static Object goHTTP(String url, Object o) throws IOException,ClassNotFoundException {
        URL serviceUrl = new URL(url);
        HttpURLConnection urlc = (HttpURLConnection)serviceUrl.openConnection();

        urlc.setRequestMethod("POST");
        urlc.setDoInput(true);
        urlc.setDoOutput(true);
        urlc.setRequestProperty("Content-Type", "application/x-javaobject");
        OutputStream os = urlc.getOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(os);
        oos.writeObject(o);
        oos.close();
        // Now wait for response
        // Error/timeout conditions throws a SocketException
        InputStream is = urlc.getInputStream();
        ObjectInputStream ois = new ObjectInputStream(is);
        Object o2 = ois.readObject();
        return o2;
    }
}
